using System;

namespace SunflowSharp.Maths
{
    /**
     * 3D axis-aligned bounding box. Stores only the minimum and maximum corner
     * points.
     */
    public class BoundingBox
    {
        private Point3 minimum;
        private Point3 maximum;

        /**
         * Creates an empty box. The minimum point will have all components set to
         * positive infinity, and the maximum will have all components set to
         * negative infinity.
         */
        public BoundingBox()
        {
            minimum = new Point3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
            maximum = new Point3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
        }

        /**
         * Creates a copy of the given box.
         * 
         * @param b bounding box to copy
         */
        public BoundingBox(BoundingBox b)
        {
            minimum = new Point3(b.minimum);
            maximum = new Point3(b.maximum);
        }

        /**
         * Creates a bounding box containing only the specified point.
         * 
         * @param p point to include
         */
        public BoundingBox(Point3 p)
            : this(p.x, p.y, p.z)
        {
        }

        /**
         * Creates a bounding box containing only the specified point.
         * 
         * @param x x coordinate of the point to include
         * @param y y coordinate of the point to include
         * @param z z coordinate of the point to include
         */
        public BoundingBox(float x, float y, float z)
        {
            minimum = new Point3(x, y, z);
            maximum = new Point3(x, y, z);
        }

        /**
         * Creates a bounding box centered around the origin.
         * 
         * @param size half edge Length of the bounding box
         */
        public BoundingBox(float size)
        {
            minimum = new Point3(-size, -size, -size);
            maximum = new Point3(size, size, size);
        }

        /**
         * Gets the minimum corner of the box. That is the corner of smallest
         * coordinates on each axis. Note that the returned reference is not cloned
         * for efficiency purposes so care must be taken not to change the
         * coordinates of the point.
         * 
         * @return a reference to the minimum corner
         */
        public Point3 getMinimum()
        {
            return minimum;
        }

        /**
         * Gets the maximum corner of the box. That is the corner of largest
         * coordinates on each axis. Note that the returned reference is not cloned
         * for efficiency purposes so care must be taken not to change the
         * coordinates of the point.
         * 
         * @return a reference to the maximum corner
         */
        public Point3 getMaximum()
        {
            return maximum;
        }

        /**
         * Gets the center of the box, computed as (min + max) / 2.
         * 
         * @return a reference to the center of the box
         */
        public Point3 getCenter()
        {
            return Point3.mid(minimum, maximum, new Point3());
        }

        /**
         * Gets a corner of the bounding box. The index scheme uses the binary
         * representation of the index to decide which corner to return. Corner 0 is
         * equivalent to the minimum and corner 7 is equivalent to the maximum.
         * 
         * @param i a corner index, from 0 to 7
         * @return the corresponding corner
         */
        public Point3 getCorner(int i)
        {
            float x = (i & 1) == 0 ? minimum.x : maximum.x;
            float y = (i & 2) == 0 ? minimum.y : maximum.y;
            float z = (i & 4) == 0 ? minimum.z : maximum.z;
            return new Point3(x, y, z);
        }

        /**
         * Gets a specific coordinate of the surface's bounding box.
         * 
         * @param i index of a side from 0 to 5
         * @return value of the request bounding box side
         */
        public float getBound(int i)
        {
            switch (i)
            {
                case 0:
                    return minimum.x;
                case 1:
                    return maximum.x;
                case 2:
                    return minimum.y;
                case 3:
                    return maximum.y;
                case 4:
                    return minimum.z;
                case 5:
                    return maximum.z;
                default:
                    return 0;
            }
        }

        /**
         * Gets the extents vector for the box. This vector is computed as (max -
         * min). Its coordinates are always positive and represent the dimensions of
         * the box along the three axes.
         * 
         * @return a refreence to the extent vector
         * @see org.sunflow.math.Vector3#Length()
         */
        public Vector3 getExtents()
        {
            return Point3.sub(maximum, minimum, new Vector3());
        }

        /**
         * Gets the surface area of the box.
         * 
         * @return surface area
         */
        public float getArea()
        {
            Vector3 w = getExtents();
            float ax = Math.Max(w.x, 0);
            float ay = Math.Max(w.y, 0);
            float az = Math.Max(w.z, 0);
            return 2 * (ax * ay + ay * az + az * ax);
        }

        /**
         * Gets the box's volume
         * 
         * @return volume
         */
        public float getVolume()
        {
            Vector3 w = getExtents();
            float ax = Math.Max(w.x, 0);
            float ay = Math.Max(w.y, 0);
            float az = Math.Max(w.z, 0);
            return ax * ay * az;
        }

        /**
         * Enlarge the bounding box by the minimum possible amount to avoid numeric
         * precision related problems.
         */
        public void enlargeUlps() {
        float eps = 0.0001f;
        minimum.x -= eps;//Math.Max(eps, Math.ulp(minimum.x));
        minimum.y -= eps;//Math.Max(eps, Math.ulp(minimum.y));
        minimum.z -= eps;//Math.Max(eps, Math.ulp(minimum.z));
        maximum.x += eps;//Math.Max(eps, Math.ulp(maximum.x));
        maximum.y += eps;//Math.Max(eps, Math.ulp(maximum.y));
        maximum.z += eps;//Math.Max(eps, Math.ulp(maximum.z));
    }

        /**
         * Returns <code>true</code> when the box has just been initialized, and
         * is still empty. This method might also return true if the state of the
         * box becomes inconsistent and some component of the minimum corner is
         * larger than the corresponding coordinate of the maximum corner.
         * 
         * @return <code>true</code> if the box is empty, <code>false</code>
         *         otherwise
         */
        public bool isEmpty()
        {
            return (maximum.x < minimum.x) || (maximum.y < minimum.y) || (maximum.z < minimum.z);
        }

        /**
         * Returns <code>true</code> if the specified bounding box intersects this
         * one. The boxes are treated as volumes, so a box inside another will
         * return true. Returns <code>false</code> if the parameter is
         * <code>null</code>.
         * 
         * @param b box to be tested for intersection
         * @return <code>true</code> if the boxes overlap, <code>false</code>
         *         otherwise
         */
        public bool intersects(BoundingBox b)
        {
            return ((b != null) && (minimum.x <= b.maximum.x) && (maximum.x >= b.minimum.x) && (minimum.y <= b.maximum.y) && (maximum.y >= b.minimum.y) && (minimum.z <= b.maximum.z) && (maximum.z >= b.minimum.z));
        }

        /**
         * Checks to see if the specified {@link org.sunflow.math.Point3 point}is
         * inside the volume defined by this box. Returns <code>false</code> if
         * the parameter is <code>null</code>.
         * 
         * @param p point to be tested for containment
         * @return <code>true</code> if the point is inside the box,
         *         <code>false</code> otherwise
         */
        public bool contains(Point3 p)
        {
            return ((p != null) && (p.x >= minimum.x) && (p.x <= maximum.x) && (p.y >= minimum.y) && (p.y <= maximum.y) && (p.z >= minimum.z) && (p.z <= maximum.z));
        }

        /**
         * Check to see if the specified point is inside the volume defined by this
         * box.
         * 
         * @param x x coordinate of the point to be tested
         * @param y y coordinate of the point to be tested
         * @param z z coordinate of the point to be tested
         * @return <code>true</code> if the point is inside the box,
         *         <code>false</code> otherwise
         */
        public bool contains(float x, float y, float z)
        {
            return ((x >= minimum.x) && (x <= maximum.x) && (y >= minimum.y) && (y <= maximum.y) && (z >= minimum.z) && (z <= maximum.z));
        }

        /**
         * Changes the extents of the box as needed to include the given
         * {@link org.sunflow.math.Point3 point}into this box. Does nothing if the
         * parameter is <code>null</code>.
         * 
         * @param p point to be included
         */
        public void include(Point3 p)
        {
            if (p != null)
            {
                if (p.x < minimum.x)
                    minimum.x = p.x;
                if (p.x > maximum.x)
                    maximum.x = p.x;
                if (p.y < minimum.y)
                    minimum.y = p.y;
                if (p.y > maximum.y)
                    maximum.y = p.y;
                if (p.z < minimum.z)
                    minimum.z = p.z;
                if (p.z > maximum.z)
                    maximum.z = p.z;
            }
        }

        /**
         * Changes the extents of the box as needed to include the given point into
         * this box.
         * 
         * @param x x coordinate of the point
         * @param y y coordinate of the point
         * @param z z coordinate of the point
         */
        public void include(float x, float y, float z)
        {
            if (x < minimum.x)
                minimum.x = x;
            if (x > maximum.x)
                maximum.x = x;
            if (y < minimum.y)
                minimum.y = y;
            if (y > maximum.y)
                maximum.y = y;
            if (z < minimum.z)
                minimum.z = z;
            if (z > maximum.z)
                maximum.z = z;
        }

        /**
         * Changes the extents of the box as needed to include the given box into
         * this box. Does nothing if the parameter is <code>null</code>.
         * 
         * @param b box to be included
         */
        public void include(BoundingBox b)
        {
            if (b != null)
            {
                if (b.minimum.x < minimum.x)
                    minimum.x = b.minimum.x;
                if (b.maximum.x > maximum.x)
                    maximum.x = b.maximum.x;
                if (b.minimum.y < minimum.y)
                    minimum.y = b.minimum.y;
                if (b.maximum.y > maximum.y)
                    maximum.y = b.maximum.y;
                if (b.minimum.z < minimum.z)
                    minimum.z = b.minimum.z;
                if (b.maximum.z > maximum.z)
                    maximum.z = b.maximum.z;
            }
        }

        public override string ToString()
        {
            return string.Format("({0}, {1}, {2}) to ({3}, {4}, {5})", minimum.x, minimum.y, minimum.z, maximum.x, maximum.y, maximum.z);
        }
    }
}